Skip to content

Conversation

braden-w
Copy link
Contributor

@braden-w braden-w commented May 29, 2025

This PR introduces isFunctionVariant() and resolveValueOrFunction() utilities to replace repetitive typeof === 'function' checks throughout the codebase, consolidating the common "value or function that computes value" pattern.

Problem and Solution

The codebase had scattered implementations of the same pattern across multiple files:

// Repetitive pattern everywhere
const data = typeof options.initialData === 'function' 
  ? options.initialData() 
  : options.initialData

const delay = typeof retryDelay === 'function'
  ? retryDelay(failureCount, error) 
  : retryDelay

This led to code duplication, inconsistency, and maintenance overhead. We also had specialized versions like resolveStaleTime() and resolveEnabled() that could be generalized.

The new utilities provide a clean, generic solution:

// Clean and intention-revealing
const data = resolveValueOrFunction(options.initialData)
const delay = resolveValueOrFunction(retryDelay, failureCount, error)

While we could inline typeof value === 'function' checks, TypeScript's type narrowing doesn't work properly with generic types in complex expressions. The isFunctionVariant() type guard provides proper type narrowing that allows resolveValueOrFunction() to safely call the function variant. Without it, TypeScript throws errors because it can't guarantee the type safety across the generic boundary.

Both utilities support zero-argument functions (() => T) and functions with parameters ((...args) => T), making them applicable to all value-or-function patterns in the codebase.

Updated implementations in query.ts, queryObserver.ts, and retryer.ts for handling initialData, retryDelay, refetchInterval, and notifyOnChangeProps. These utilities can replace existing resolveStaleTime() and resolveEnabled() functions in future iterations.

Summary by CodeRabbit

  • New Features

    • Placeholder data can now be provided either directly or via a function.
  • Refactor

    • Option resolution unified across enabled checks, stale timing, initial/placeholder data, retry delays, and notification behavior.
  • Chores

    • Removed legacy option helpers and exported types; introduced a single resolve-style utility and consolidated related timing and observer/client logic.

@braden-w braden-w changed the title refactor(core): add generic utilities for resolving value-or-function patterns refactor(core): add generic utilities for resolving value-or-function patterns, replace specialized resolveStaleTime and resolveEnabled May 29, 2025
Copy link

nx-cloud bot commented May 29, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit ae7a70b

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 2m 22s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 22s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-01 11:11:08 UTC

@braden-w braden-w force-pushed the refactor/resolveValueOrFunction branch from df0249f to 2421a38 Compare May 29, 2025 04:59
… patterns, replace specialized `resolveStaleTime` and `resolveEnabled`

This commit introduces `isFunctionVariant()` and `resolveValueOrFunction()` utilities to replace repetitive `typeof === 'function'` checks throughout the codebase, consolidating the common "value or function that computes value" pattern.
@braden-w braden-w force-pushed the refactor/resolveValueOrFunction branch from 409a222 to b28e950 Compare May 29, 2025 05:05
… NonFunctionGuard

This simplification is possible due to the introduction of generic runtime
utilities that handle value-or-function resolution. The new `resolveValueOrFunction()` utility handles the distinction between direct values and functions at runtime with proper type safety, eliminating the  need for complex type-level guards.
@braden-w braden-w force-pushed the refactor/resolveValueOrFunction branch from b28e950 to cd59cc4 Compare May 29, 2025 16:31
Copy link

pkg-pr-new bot commented May 29, 2025

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9212

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9212

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9212

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9212

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9212

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9212

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9212

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9212

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9212

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9212

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9212

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9212

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9212

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9212

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9212

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9212

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9212

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9212

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9212

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9212

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9212

commit: ae7a70b

@braden-w braden-w force-pushed the refactor/resolveValueOrFunction branch from 9d15984 to 427c977 Compare May 29, 2025 16:48
Copy link
Collaborator

@TkDodo TkDodo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally agree with the direction, please have a look at the comments though

@@ -58,7 +58,8 @@
"@angular/platform-browser-dynamic": "^20.0.0",
"@tanstack/angular-query-experimental": "workspace:*",
"eslint-plugin-jsdoc": "^50.5.0",
"npm-run-all2": "^5.0.0"
"npm-run-all2": "^5.0.0",
"tsup": "^8.4.0"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was adding tsup necessary here? I don’t think angular uses tsup for bundling 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed! In earlier commits, the lack of tsup was blocking builds.

): T {
return isFunctionVariant(value) ? value(...args) : value
}

export function functionalUpdate<TInput, TOutput>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we use resolveValueOrFunction here as well, as this also uses a typeof updater === 'function' check ?

* const delay = resolveValueOrFunction(retryDelay, failureCount, error)
* ```
*/
export function resolveValueOrFunction<T, TArgs extends Array<any>>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a name like resolveOption would be descriptive enough

@TkDodo
Copy link
Collaborator

TkDodo commented Jun 2, 2025

hm, the failing pipeline shows a lot of type-errors

braden-w and others added 6 commits August 11, 2025 00:50
Remove specialized resolveEnabled function and consolidate functionality
into the generic resolveOption utility. Both functions performed identical
operations but resolveEnabled was type-specific to query enabled state.

- Remove resolveEnabled function from utils.ts
- Update queryObserver.ts to use resolveOption instead
- Remove unused Enabled type import
- Maintain identical functionality and type safety
@TkDodo
Copy link
Collaborator

TkDodo commented Aug 19, 2025

it’s been two months, there’s still a failing pipeline and conflicts. please let me know if you intend to finish this PR, because it doesn’t look like it’s close to working. I’ll close it otherwise.

@braden-w
Copy link
Contributor Author

Thanks for the ping! Will update it and get back to you by EOD.

Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Walkthrough

Replaced specialized per-option resolvers with a single exported resolveOption across query-core; updated query, observer, client, retryer, utils, and types to use it. Removed InitialDataFunction and NonFunctionGuard, made PlaceholderDataFunction non-exported, and relaxed placeholderData typing.

Changes

Cohort / File(s) Summary of changes
Core query logic
packages/query-core/src/query.ts
Replaced resolveEnabled/resolveStaleTime usages with resolveOption; updated isActive/isStatic checks, initialData and initialDataUpdatedAt resolution, and removed InitialDataFunction usage/import.
Observer logic
packages/query-core/src/queryObserver.ts
Consolidated resolution of enabled, staleTime, refetchInterval, placeholderData, notifyOnChangeProps to resolveOption; updated setOptions, update flow, optimistic/result construction; removed old resolvers and export/import of PlaceholderDataFunction.
Client fetch flow
packages/query-core/src/queryClient.ts
Switched staleTime resolution to resolveOption in ensureQueryData and fetchQuery; adjusted imports; control flow and public signatures unchanged.
Retry handling
packages/query-core/src/retryer.ts
Replaced manual function/number handling for retryDelay with resolveOption(retryDelay, failureCount, error); added import; retry logic otherwise unchanged.
Types and public API
packages/query-core/src/types.ts
Removed NonFunctionGuard and InitialDataFunction exports; made PlaceholderDataFunction non-exported; changed QueryObserverOptions.placeholderData to `TQueryData
Utilities
packages/query-core/src/utils.ts
Added exported `resolveOption<T, TArgs extends Array>(value

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Consumer
  participant QO as QueryObserver
  participant Q as Query
  participant QC as QueryClient
  participant U as utils.resolveOption
  participant R as Retryer

  C->>QO: setOptions(options)
  QO->>U: resolveOption(options.enabled, query)
  U-->>QO: isEnabled
  QO->>U: resolveOption(options.staleTime, query)
  U-->>QO: staleTime
  QO->>Q: request initial/placeholder data
  Q->>U: resolveOption(options.initialData, query)
  U-->>Q: initialData

  alt Needs fetch
    QO->>QC: fetchQuery(queryKey, options)
    QC->>U: resolveOption(options.staleTime, query)
    U-->>QC: staleTime
    QC->>R: start retryer
    R->>U: resolveOption(options.retryDelay, failureCount, error)
    U-->>R: delay
    R-->>QC: result/error
    QC-->>QO: result
    QO-->>C: notify result
  else No fetch
    QO-->>C: notify cached result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit nibbles at tangled threads,
One helper now settles all option beds.
Stale times and placeholders hum in tune,
Retries count down beneath the moon.
Thump—refactor hopped away by noon. 🐇


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8854b1a and ae7a70b.

📒 Files selected for processing (2)
  • packages/query-core/src/query.ts (5 hunks)
  • packages/query-core/src/retryer.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/query-core/src/retryer.ts
  • packages/query-core/src/query.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/query-core/src/types.ts (1)

169-177: PlaceholderDataFunction type is now internal but still referenced in public API

The PlaceholderDataFunction type has been changed from exported to internal (no export keyword), but it's still being used in the public QueryObserverOptions interface at lines 424-426. This will cause TypeScript compilation errors for consumers who need to understand or reference this type.

Apply this diff to fix the type visibility issue:

-type PlaceholderDataFunction<
+export type PlaceholderDataFunction<
   TQueryFnData = unknown,
   TError = DefaultError,
   TQueryData = TQueryFnData,
   TQueryKey extends QueryKey = QueryKey,
 > = (
   previousData: TQueryData | undefined,
   previousQuery: Query<TQueryFnData, TError, TQueryData, TQueryKey> | undefined,
 ) => TQueryData | undefined

Alternatively, if the intent is to keep it internal, consider inlining the function signature directly in the placeholderData property definition.

♻️ Duplicate comments (1)
packages/query-core/src/utils.ts (1)

168-175: Use resolveOption here for consistency (duplicate of earlier feedback).

This is the same “value or updater function” pattern. Reusing resolveOption or the local isFunctionVariant keeps the file consistent and DRY.

Apply this diff:

 export function functionalUpdate<TInput, TOutput>(
   updater: Updater<TInput, TOutput>,
   input: TInput,
 ): TOutput {
-  return typeof updater === 'function'
-    ? (updater as (_: TInput) => TOutput)(input)
-    : updater
+  return isFunctionVariant<Updater<TInput, TOutput>, [TInput]>(updater)
+    ? (updater as (_: TInput) => TOutput)(input)
+    : (updater as TOutput)
 }

Note: This still shares the same ambiguity if TOutput is itself a function. If you adopt NonFunctionGuard for resolveOption, consider mirroring that here for true parity.

🧹 Nitpick comments (2)
packages/query-core/src/query.ts (1)

692-698: Consider adding type safety for InitialDataFunction pattern

While the resolveOption utility handles the function-or-value pattern generically, the removal of the InitialDataFunction type means we lose some type safety. The current implementation will accept any function, not just zero-argument functions.

Consider adding a type constraint to ensure initialData and initialDataUpdatedAt functions don't accidentally accept parameters:

-  const data = resolveOption(options.initialData)
+  const data = resolveOption(options.initialData as TData | (() => TData | undefined))
 
   const hasData = data !== undefined
 
   const initialDataUpdatedAt = hasData
-    ? resolveOption(options.initialDataUpdatedAt)
+    ? resolveOption(options.initialDataUpdatedAt as number | (() => number | undefined) | undefined)
     : 0
packages/query-core/src/utils.ts (1)

80-116: Tighten the type guard: return a function typed to T, not any.

Returning any from the predicate unnecessarily weakens type inference in downstream callers (including resolveOption). Have the predicate assert (...args: TArgs) => T and prefer unknown[] over Array<any>.

Apply this diff:

-function isFunctionVariant<T, TArgs extends Array<any> = []>(
-  value: T | ((...args: TArgs) => any),
-): value is (...args: TArgs) => any {
+function isFunctionVariant<T, TArgs extends unknown[] = []>(
+  value: T | ((...args: TArgs) => T),
+): value is (...args: TArgs) => T {
   return typeof value === 'function'
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6a85584 and d6ea2f1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • packages/query-core/src/query.ts (4 hunks)
  • packages/query-core/src/queryClient.ts (3 hunks)
  • packages/query-core/src/queryObserver.ts (14 hunks)
  • packages/query-core/src/retryer.ts (2 hunks)
  • packages/query-core/src/types.ts (2 hunks)
  • packages/query-core/src/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
packages/query-core/src/queryClient.ts (2)
packages/query-core/src/queryObserver.ts (2)
  • query (680-697)
  • resolveOption (364-368)
packages/query-core/src/utils.ts (1)
  • resolveOption (161-166)
packages/query-core/src/retryer.ts (2)
packages/query-core/src/queryObserver.ts (1)
  • resolveOption (364-368)
packages/query-core/src/utils.ts (1)
  • resolveOption (161-166)
packages/query-core/src/queryObserver.ts (1)
packages/query-core/src/utils.ts (1)
  • resolveOption (161-166)
packages/query-core/src/query.ts (2)
packages/query-core/src/queryObserver.ts (1)
  • resolveOption (364-368)
packages/query-core/src/utils.ts (1)
  • resolveOption (161-166)
packages/query-core/src/utils.ts (1)
packages/query-core/src/queryObserver.ts (1)
  • resolveOption (364-368)
🔇 Additional comments (14)
packages/query-core/src/queryClient.ts (2)

158-159: LGTM! Clean replacement of resolveStaleTime with resolveOption

The change correctly replaces the specialized resolveStaleTime with the generic resolveOption utility, maintaining the same functionality while improving code consistency.


365-365: LGTM! Consistent use of resolveOption

The replacement maintains the same behavior while using the unified resolveOption utility.

packages/query-core/src/retryer.ts (1)

169-169: LGTM! Simplified delay resolution

The change correctly replaces the manual conditional logic with the generic resolveOption utility, making the code more concise and consistent with the rest of the codebase.

packages/query-core/src/query.ts (2)

257-257: LGTM! Consistent enabled option resolution

The change correctly replaces resolveEnabled with the generic resolveOption utility.


276-276: LGTM! Consistent staleTime resolution

The change correctly replaces resolveStaleTime with the generic resolveOption utility.

packages/query-core/src/queryObserver.ts (7)

158-159: LGTM! Proper validation with resolveOption

The validation correctly uses resolveOption to check the resolved value when enabled is a function, maintaining the same validation logic.


202-205: LGTM! Consistent comparison of resolved option values

The changes correctly compare the resolved values of enabled and staleTime options using resolveOption, ensuring accurate change detection.


216-217: LGTM! Consistent enabled option comparison

The change maintains proper change detection for the enabled option using the unified utility.


345-345: LGTM! Consistent staleTime and refetchInterval resolution

All three changes correctly use resolveOption for computing stale timeout and refetch interval values.

Also applies to: 366-366, 377-377


485-489: LGTM! Clean placeholder data resolution

The change correctly uses resolveOption to handle both direct values and functions for placeholder data, properly passing the previous data and query as arguments when needed.


568-568: LGTM! Consistent option resolution in result creation

Both changes correctly use resolveOption for isEnabled flag and notifyOnChangeProps resolution.

Also applies to: 652-652


730-730: LGTM! Comprehensive update of all conditional checks

All the helper functions have been consistently updated to use resolveOption for enabled and staleTime checks, maintaining the correct behavior while using the unified utility.

Also applies to: 755-758, 773-773, 784-785

packages/query-core/src/utils.ts (2)

1-2: LGTM: type-only imports are correct and colocated.

The new type imports for Mutation, Query, and FetchOptions are valid here and used below. No issues.


117-160: Docs/readability: solid API docs and examples.

Clear, actionable, and covers both zero-arg and variadic cases. Nice.

Comment on lines +161 to +166
export function resolveOption<T, TArgs extends Array<any>>(
value: T | ((...args: TArgs) => T),
...args: TArgs
): T {
return isFunctionVariant(value) ? value(...args) : value
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Critical: resolveOption will eagerly invoke function-typed values (ambiguous when T itself is a function).

If T can be a function type (e.g., TData is a function and users provide placeholderData or initialData as a function value), resolveOption will treat that as the “function variant” and call it. This is the exact ambiguity NonFunctionGuard previously prevented. This change can introduce runtime bugs and unexpected side-effects.

Two concrete fixes (pick one):

  • Reintroduce a guard to disallow function-shaped T as direct values in this resolver (preferred for safety).
  • Alternatively, add a second API for “allow function values” and use it only where safe, but that complicates call-sites.

Apply this diff to constrain and keep strong typing (also avoids the any fallback from the guard):

-export function resolveOption<T, TArgs extends Array<any>>(
-  value: T | ((...args: TArgs) => T),
-  ...args: TArgs
-): T {
-  return isFunctionVariant(value) ? value(...args) : value
-}
+export function resolveOption<T, TArgs extends unknown[] = []>(
+  value: NonFunctionGuard<T> | ((...args: TArgs) => T),
+  ...args: TArgs
+): T {
+  return typeof value === 'function'
+    ? (value as (...args: TArgs) => T)(...args)
+    : (value as T)
+}

Add the missing type where you keep your utility types (preferred: in types.ts) and import it here:

export type NonFunctionGuard<T> = T extends Function ? never : T

Then extend the types import at the top of this file to include NonFunctionGuard.

To reduce regressions, please verify we didn’t reintroduce the “function value invoked” bug for options that can carry function-typed data:


🏁 Script executed:

#!/bin/bash
# 1) Find any lingering usage of removed helpers that might mask the issue.
rg -nP -C2 --type=ts '\bresolve(StaleTime|Enabled)\b'

# 2) Locate option types that allow function-typed data (high-risk with generic resolver).
rg -nP -C3 --type=ts '(InitialData|PlaceholderData)\b'

# 3) Grep for call sites of resolveOption with those options.
rg -nP -C3 --type=ts '\bresolveOption\s*\('

# 4) Heuristic: look for placeholderData / initialData assigned a function value (arrow/functions),
#    which would be incorrectly invoked if T is function-shaped.
rg -nPU -C2 --type=ts '(placeholderData|initialData)\s*:\s*(\(\s*[^)]*\)\s*=>|function\s*\(|[\w$]+\s*=>)'

Length of output: 160239


Critical: resolveOption eagerly invokes any function values, including those meant as data when T is itself a function type

Currently,

export function resolveOption<T, TArgs extends Array<any>>(
  value: T | ((...args: TArgs) => T),
  …args: TArgs
): T {
  return isFunctionVariant(value) ? value(...args) : value
}

will treat a direct function “value” (when T extends Function) as the callback variant and invoke it, re-introducing the ambiguity that NonFunctionGuard previously prevented.

Affected call sites include:

  • packages/query-core/src/utils.ts (resolveOption, lines 161–166)
  • packages/query-core/src/query.ts (initialData resolution)
  • packages/query-core/src/queryObserver.ts (placeholderData, enabled, staleTime, refetchInterval, notifyOnChangeProps)
  • packages/query-core/src/retryer.ts (retryDelay resolution)

Two concrete fixes (choose one):

  • Reintroduce a guard for non-function T values (preferred for type safety):

-export function resolveOption<T, TArgs extends unknown[]>(
+export function resolveOption<T, TArgs extends unknown[] = []>(

  • value: T | ((...args: TArgs) => T),
  • value: NonFunctionGuard | ((...args: TArgs) => T),
    ): T {
    return typeof value === 'function'
    ? (value as (...args: TArgs) => T)(…args)
    : (value as T)
    }
And add to your shared types (e.g. types.ts):  
```ts
export type NonFunctionGuard<T> = T extends Function ? never : T
  • Alternatively, introduce a second API—e.g. resolveOptionAllowFunction—for use cases that legitimately carry function-typed T, and keep the existing resolveOption safe for all other scenarios.

Both approaches restore strong typing and prevent accidental invocation of function-type data. Please implement one of these fixes across all resolveOption call sites.

🤖 Prompt for AI Agents
In packages/query-core/src/utils.ts around lines 161–166, resolveOption eagerly
invokes any function value which incorrectly calls function-typed data; update
implementation and types to prevent invoking values whose T is itself a
Function. Fix by introducing a NonFunctionGuard<T> = T extends Function ? never
: T in shared types and change resolveOption signature to accept value:
NonFunctionGuard<T> | ((...args: TArgs) => T) so direct function-typed data is
treated as data, not invoked; alternatively implement a second API
resolveOptionAllowFunction for the rare cases that must allow function-typed T,
and then update all affected call sites (packages/query-core/src/query.ts
initialData resolution, packages/query-core/src/queryObserver.ts for
placeholderData, enabled, staleTime, refetchInterval, notifyOnChangeProps, and
packages/query-core/src/retryer.ts retryDelay resolution) to use the safe
resolveOption or the explicit allow-function variant accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/query-core/src/utils.ts (2)

172-172: Addressed prior note: functionalUpdate now uses resolveOption.

This replaces the typeof check with the new utility as requested in earlier review comments.


117-166: Critical: resolveOption will eagerly invoke function-typed values when T itself is a function (ambiguous semantics).

If T is a function type, direct function “values” will be treated as the callback variant and invoked. This is the ambiguity that the former NonFunctionGuard prevented and can lead to runtime bugs and surprising side-effects. This also aligns with prior review feedback.

Fix by constraining T to non-function values for the direct-value branch and defaulting TArgs to unknown[] to improve inference:

-export function resolveOption<T, TArgs extends Array<any>>(
-  value: T | ((...args: TArgs) => T),
-  ...args: TArgs
-): T {
-  return isFunctionVariant(value) ? value(...args) : value
-}
+export function resolveOption<T, TArgs extends unknown[] = []>(
+  value: NonFunctionGuard<T> | ((...args: TArgs) => T),
+  ...args: TArgs
+): T {
+  return typeof value === 'function'
+    ? (value as (...args: TArgs) => T)(...args)
+    : (value as T)
+}

Also add the missing utility type and import:

  • In types.ts (exported somewhere central):
// Add this near existing utility types
export type NonFunctionGuard<T> = T extends Function ? never : T
  • Update the import in this file to pull it in (adjust your existing types import):
import type {
  DefaultError,
  FetchStatus,
  MutationKey,
  MutationStatus,
  QueryFunction,
  QueryKey,
  QueryOptions,
+ NonFunctionGuard,
} from './types'

If you need an escape hatch for cases where function-shaped T must be allowed as a direct value, introduce a second API, e.g. resolveOptionAllowFunction, and use it only where intended.

Verification script (locates call sites and checks for the guard’s presence):

#!/bin/bash
# 1) Confirm the NonFunctionGuard type exists (or not).
rg -nP --type=ts '\btype\s+NonFunctionGuard\b|export\s+type\s+NonFunctionGuard\b' -C2

# 2) List all resolveOption call sites with context to audit ambiguous usages.
rg -nP --type=ts '\bresolveOption\s*\(' -C3

# 3) Heuristic: show spots where the resolved T could plausibly be a function-typed value
#    (look for option names that might carry function-shaped data).
rg -nP --type=ts -C2 '(initialData|placeholderData|notifyOnChangeProps|refetchInterval|retryDelay)\b'
🧹 Nitpick comments (1)
packages/query-core/src/utils.ts (1)

80-116: Prefer unknown[] over Array and avoid leaking any from the guard.

Tighten the generics to unknown[] and use unknown instead of any for the guard’s return type. This improves type safety without changing behavior.

Apply:

-function isFunctionVariant<T, TArgs extends Array<any> = []>(
-  value: T | ((...args: TArgs) => any),
-): value is (...args: TArgs) => any {
+function isFunctionVariant<T, TArgs extends unknown[] = []>(
+  value: T | ((...args: TArgs) => unknown),
+): value is (...args: TArgs) => unknown {
   return typeof value === 'function'
 }

Optional: if you foresee external usage for narrowing, consider exporting this guard. Otherwise keeping it internal is fine.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d6ea2f1 and 6fff343.

📒 Files selected for processing (1)
  • packages/query-core/src/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/query-core/src/utils.ts (1)
packages/query-core/src/queryObserver.ts (1)
  • resolveOption (364-368)
🔇 Additional comments (1)
packages/query-core/src/utils.ts (1)

1-2: Type-only imports for Mutation/FetchOptions/Query — good move.

This prevents runtime cycles and keeps bundles clean. No concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants